Skip to content

fix(sounds): de-duplicate notification sounds across browser tabs - #831

Open
garrappachc wants to merge 2 commits into
masterfrom
feat/dedupe-notification-sounds
Open

garrappachc wants to merge 2 commits into
masterfrom
feat/dedupe-notification-sounds

Conversation

@garrappachc

@garrappachc garrappachc commented Aug 31, 2026

Copy link
Copy Markdown
Member

Why

A player with several tabs open heard the same notification sound from every tab at once — each tab holds its own websocket and independently reached playSound(). This de-duplicates playback so exactly one tab beeps.

How

Tabs race for a Web Lock keyed by the sound id (navigator.locks.request(soundId, { ifAvailable: true }, …)). The winner plays; everyone else gets null back and skips. The lock is held ~250ms after playing so that tabs reacting to the same websocket message lose the race, while a notification arriving later still gets through.

playSound() resumes the audio context before racing and drops out if it isn't running. That means a tab which can't unlock its audio never wins the lock and swallows the notification — and, for free, the focused tab usually wins, since its context is already running while a backgrounded tab has to spend a resume first.

If navigator.locks is unavailable (older Safari), it degrades to the previous "all tabs play" behaviour — the same baseline as BroadcastChannel, which the mention path still uses.

The mention clear path broadcasts over a BroadcastChannel, so reading chat in one tab clears the sound, the title prefix and the tab badge in the others. The sound half matters most because that clip is ~6s where the rest are ~1.5s.

Scope / notes

  • Applies to all four notification sounds (mention, ready-up, substitution, fight).
  • playSound() now requires the element to carry an id — it is the lock key. All four sound elements have one and every call path resolves via getElementById, but an id-less [data-sound-src] element would now stay silent.
  • No new telemetry: existing capability metrics (audioReadyaudio_blocked_players.count, ready_up.notified.count{audioBlocked}) stay valid; there is no client→server actuation signal and this adds none.
  • No automated tests: the front-end has no unit-test harness (vitest runs on node, no DOM env), and Playwright cannot observe real audio output. The lock mechanism itself was checked out-of-tree by driving three tabs of one browser profile — simultaneous burst plays exactly once, a later notification still plays, distinct sounds don't block each other. Audio verified manually: two tabs, one beep; clearing in either tab stops the mention and drops the star in both.
  • Desktop-notification duplication (new Notification() with no tag) is a separate, trivial follow-up.

History

The first pass coordinated over a BroadcastChannel election — each tab announced a claim, waited a 60ms window to collect competitors, and the highest-ranked (focused > visible > hidden) played. That was replaced because hidden pages clamp setTimeout to ~1s, which is exactly the scenario the feature targets: the player is alt-tabbed into TF2 waiting on ready-up, so every tab's claim window stretches and the beep lands late. Web Locks put no timer on the latency path, release automatically if a tab crashes, and the priority ordering they give up was inaudible anyway — the tabs share the same speakers.

Every open tab held its own websocket and independently played notification
sounds, so a player with N tabs heard N simultaneous beeps. Coordinate playback
over a BroadcastChannel: each sound is claimed by the tabs, and only the
highest-priority audio-capable tab (focused > visible > hidden) actually plays.
The mention clear now also stops the sound cross-tab, since that clip is ~6s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@garrappachc
garrappachc force-pushed the feat/dedupe-notification-sounds branch from 5a2f715 to 550dba8 Compare August 31, 2026 14:42
@garrappachc garrappachc changed the title feat(sounds): de-duplicate notification sounds across browser tabs fix(sounds): de-duplicate notification sounds across browser tabs Aug 31, 2026
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Playwright test results

passed  246 passed

Details

stats  246 tests across 47 suites
duration  12 minutes, 48 seconds
commit  380a649

The BroadcastChannel election worked but leaned on a 60ms setTimeout window, and
hidden pages clamp timers to ~1s -- exactly the case this fixes, since the point is
that the player is alt-tabbed away. navigator.locks does the same job natively, has
the same browser baseline, and puts no timer on the latency path.

Resuming the audio context before racing replaces the isAudioBlocked() heuristic
with the empirical answer: a tab that cannot unlock its audio drops out instead of
winning and swallowing the notification. That also makes the focused tab win most
races for free, since its context is usually already running.

Clearing a mention now clears the star and the badge in the other tabs too, not
just the sound -- both halves of "the user read the chat".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CBup4reCuf3c49KUkprEF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant